/*
** Copyright (c) 2010  Kimmo 'Rainy' Pekkola
**
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation, either version 3 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program.  If not, see http://www.gnu.org/licenses.
*/

#include "scanwindow.h"
#include "mainwindow.h"
#include "sortfilter.h"

//-----------------------------------------------------------------------------
/**
** Constructorb
**
** \param pParent The parent for the main window (usually NULL)
*/
CScanWindow::CScanWindow(QWidget* pParent)
    : QMainWindow(pParent)
{
    setWindowTitle("Storage Usage");

    m_pTreeWidget = NULL;
    m_pDelegate = NULL;
    m_pModel = NULL;
    m_pSortModel = NULL;
    m_pUsageGrid = NULL;
    m_pScanThread = NULL;
    m_pCurrentItem = NULL;
    m_pSelectedItem = NULL;
    m_pPathLabel = NULL;
    m_pProgress = NULL;
    m_pTextFilter = NULL;
    m_pCancelButton = NULL;
    m_bScanApps = false;
    m_pSearchAction = NULL;
    m_bSearchActive = false;

    qRegisterMetaType<ColorMap>("ColorMap");

#ifdef Q_WS_MAEMO_5
    setAttribute(Qt::WA_Maemo5StackedWindow);
#endif

    QSettings settings;
    m_Limit = settings.value("limit", DEFAULT_MAX_LEVEL).toInt();
}

//-----------------------------------------------------------------------------
/**
** Destructor
*/
CScanWindow::~CScanWindow()
{
    QSettings settings;
    settings.setValue("limit", m_Limit);

    if (m_pModel)
    {
        m_pModel->clear();
    }

    if (m_pScanThread)
    {
        m_pScanThread->cancel();
        m_pScanThread->wait(5000);      // Wait max 5 secs for the thread to quit
        delete m_pScanThread;
    }
}

//-----------------------------------------------------------------------------
/**
** Initializes the widgets.
*/
void CScanWindow::initialize(QString strFolder, bool bScanApps)
{
    m_strFolder = strFolder;
    m_bScanApps = bScanApps;

    QWidget* pRoot = new QWidget(this);
    QSplitter* pSplitter = new QSplitter(pRoot);

    // The folder tree
    m_pTreeWidget = new QTreeView(pSplitter);
    m_pTreeWidget->setProperty("FingerScrollable", true);
    m_pTreeWidget->setHeaderHidden(true);
    m_pTreeWidget->setRootIsDecorated(false);
    m_pTreeWidget->setAllColumnsShowFocus(true);
    m_pTreeWidget->header()->setMovable(false);
    m_pDelegate = new CDelegate(this);
    m_pTreeWidget->setItemDelegate(m_pDelegate);
    m_pTreeWidget->setEditTriggers(QAbstractItemView::NoEditTriggers);
    pSplitter->addWidget(m_pTreeWidget);

    m_pModel = new QStandardItemModel(this);
    m_pSortModel = new CSortFilter(this);
    m_pSortModel->setSourceModel(m_pModel);
    m_pSortModel->setDynamicSortFilter(true);
    m_pSortModel->setSortRole(SizeRole);
    m_pSortModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
    m_pSortModel->sort(0, Qt::DescendingOrder);
    m_pTreeWidget->setModel(m_pSortModel);

    // The usage grid
    m_pUsageGrid = new CUsageGrid(pSplitter, m_pTreeWidget, m_Limit);
    pSplitter->addWidget(m_pUsageGrid);

    QVBoxLayout* pLayout = new QVBoxLayout();
    pLayout->addWidget(pSplitter, 1);

    QList<int> listSizes;
    listSizes << 100 << 100;
    pSplitter->setSizes(listSizes);

    QHBoxLayout* bBottomLayout = new QHBoxLayout();
    m_pPathLabel = new QLabel(pRoot);
    bBottomLayout->addWidget(m_pPathLabel, 1);
    m_pProgress = new QProgressBar(pRoot);
    bBottomLayout->addWidget(m_pProgress, 1);
    m_pTextFilter = new QLineEdit(pRoot);
    bBottomLayout->addWidget(m_pTextFilter, 1);
    m_pCancelButton = new QToolButton(pRoot);
    m_pCancelButton->setIcon(QIcon(":/cancel.png"));
    m_pCancelButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
    m_pCancelButton->resize(32, 32);
    bBottomLayout->addWidget(m_pCancelButton, 0);
    pLayout->addLayout(bBottomLayout, 0);

    m_pProgress->hide();
    m_pTextFilter->hide();
    m_pCancelButton->hide();

    pRoot->setLayout(pLayout);
    setCentralWidget(pRoot);

    // Create menu items
    QAction* rescanAction = new QAction(tr("Rescan"), this);
    m_pSearchAction = new QAction(tr("Search"), this);
    m_pSearchAction->setCheckable(true);
    m_pSearchAction->setEnabled(false);

    // Add item into menu
#if defined(Q_WS_MAEMO_5)
    menuBar()->addAction(rescanAction);
    menuBar()->addAction(m_pSearchAction);
#else
    // else File menu
    QMenu* menu = new QMenu(tr("File"), this);
    menu->addAction(rescanAction);
    menu->addAction(m_pSearchAction);

    QAction* exitAction = new QAction(tr("Close"), this);
    menu->addAction(exitAction);
    connect(exitAction, SIGNAL(triggered()), this, SLOT(close()));

    menuBar()->addMenu(menu);
#endif

    bool bOk;
    bOk = connect(rescanAction, SIGNAL(triggered()), this, SLOT(onRescan()));
    Q_ASSERT(bOk);
    bOk = connect(m_pSearchAction, SIGNAL(triggered()), this, SLOT(onSearch()));
    Q_ASSERT(bOk);
    bOk = connect(m_pTreeWidget->selectionModel(), SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)), this, SLOT(onItemSelectionChanged(const QModelIndex&, const QModelIndex&)));
    Q_ASSERT(bOk);
    bOk = connect(m_pTreeWidget, SIGNAL(doubleClicked(const QModelIndex&)), this, SLOT(onItemDoubleClicked(const QModelIndex&)));
    Q_ASSERT(bOk);
    bOk = connect(m_pTreeWidget, SIGNAL(expanded(const QModelIndex&)), this, SLOT(onItemExpanded(const QModelIndex&)));
    Q_ASSERT(bOk);
}

//-----------------------------------------------------------------------------
/**
** Starts the scan thread on the given folder
*/
void CScanWindow::startScan()
{
    m_pTextFilter->clear();
    m_pTextFilter->hide();
    m_pSearchAction->setChecked(false);
    m_bSearchActive = false;
    m_pSortModel->setFilterFixedString("");

    if (m_pScanThread)
    {
        m_pScanThread->cancel();
        m_pScanThread->wait(5000);      // Wait max 5 secs for the thread to quit
        delete m_pScanThread;
        m_pScanThread = NULL;
    }

    m_pModel->clear();
    m_pUsageGrid->clear();
    m_pCurrentItem = NULL;

    m_pScanThread = new CScanThread(this, m_strFolder);

    bool bOk = false;
    bOk = connect(m_pScanThread, SIGNAL(folderFound(const QString&, int)), this, SLOT(onFolderFound(const QString&, int)));
    Q_ASSERT(bOk);
    bOk = connect(m_pScanThread, SIGNAL(folderScanned()), this, SLOT(onFolderScanned()));
    Q_ASSERT(bOk);
    bOk = connect(m_pScanThread, SIGNAL(fileFound(const QString&, const QString&, quint64, int)), this, SLOT(onFileFound(const QString&, const QString&, quint64, int)));
    Q_ASSERT(bOk);
    bOk = connect(m_pScanThread, SIGNAL(progressMaximum(int)), this, SLOT(onProgressMaximum(int)));
    Q_ASSERT(bOk);
    bOk = connect(m_pScanThread, SIGNAL(progressValue(QString, int)), this, SLOT(onProgressValue(QString, int)));
    Q_ASSERT(bOk);
    bOk = connect(m_pCancelButton, SIGNAL(clicked()), m_pScanThread, SLOT(cancel()));
    Q_ASSERT(bOk);
    bOk = connect(m_pTextFilter, SIGNAL(textChanged(const QString&)), this, SLOT(onTextChanged(const QString&)));
    Q_ASSERT(bOk);

    m_pScanThread->start();
}

///////////////////////////////////////////////////////////////////////////////
/// SLOTS
///////////////////////////////////////////////////////////////////////////////

//-----------------------------------------------------------------------------
/**
** Slot which gets called when the menu item is selected. Restart the to scan.
*/
void CScanWindow::onRescan()
{
    startScan();
}

//-----------------------------------------------------------------------------
/**
** Slot which gets called when the menu item is selected. Shows/hides the search.
*/
void CScanWindow::onSearch()
{
    bool bOk;
    if (m_bSearchActive)
    {
        bOk = disconnect(m_pCancelButton, SIGNAL(clicked()), this, SLOT(onSearch()));
        Q_ASSERT(bOk);

        m_pSearchAction->setChecked(false);
        m_pTextFilter->clear();
        m_pTextFilter->hide();
        m_pCancelButton->hide();
        m_pPathLabel->show();
        m_bSearchActive = false;
    }
    else
    {
        bOk = connect(m_pCancelButton, SIGNAL(clicked()), this, SLOT(onSearch()));
        Q_ASSERT(bOk);

        m_pSearchAction->setChecked(true);
        m_pTextFilter->show();
        m_pCancelButton->show();
        m_pPathLabel->hide();
        m_pSortModel->setFilterFixedString("");
        m_bSearchActive = true;
        m_pTextFilter->setFocus();
    }
}

//-----------------------------------------------------------------------------
/**
** Slot which gets called when filter text is changed.
*/
void CScanWindow::onTextChanged(const QString& strText)
{
    m_pSortModel->setFilterFixedString(strText);
}

//-----------------------------------------------------------------------------
/**
** Slot which gets called when a new folder has been found. Adds the folder to
** the tree widget.
**
** \param strFolder The full path to the folder.
** \param level The current hierarchy level.
*/
void CScanWindow::onFolderFound(const QString& strFolder, int level)
{
    if (m_pModel)
    {
        QStandardItem* pItem = new QStandardItem();
        if (level < m_Limit)
        {
            pItem->setData(TYPE_FOLDER, TypeRole);
        }
        else
        {
            pItem->setData(TYPE_UNSCANNED_FOLDER, TypeRole);
        }
        pItem->setData(strFolder, PathRole);
        pItem->setData(0, SizeRole);

        if (m_pCurrentItem)
        {
            QStringList listFolder = strFolder.split('/');
            pItem->setData(listFolder.last(), Qt::DisplayRole);
            m_pCurrentItem->appendRow(pItem);
        }
        else
        {
            pItem->setData(strFolder, Qt::DisplayRole);
            m_pModel->appendRow(pItem);
            m_pTreeWidget->expandToDepth(0);
        }
        m_pCurrentItem = pItem;
    }
}

//-----------------------------------------------------------------------------
/**
** Slot which gets called when the current folder has been scanned. Calculates
** the total size from the contained files and subfolders.
*/
void CScanWindow::onFolderScanned()
{
    if (m_pCurrentItem)
    {
        // Add the subfolders to the total (files are already added)
        quint64 size = m_pCurrentItem->data(SizeRole).toULongLong(NULL);
        QString strFilePath = m_pCurrentItem->data(PathRole).toString();

        if (m_pCurrentItem->data(TypeRole).toInt() == TYPE_UNSCANNED_FOLDER && m_pCurrentItem->rowCount() == 0)
        {
            m_pCurrentItem->appendRow(new QStandardItem());  // Temp item to show the decoration
        }

        m_pCurrentItem = m_pCurrentItem->parent();
        if (m_pCurrentItem == NULL && m_pUsageGrid && m_pDelegate)
        {
            // This was the last item -> Create the usage grid
            m_pUsageGrid->initialize(m_pModel->item(0, 0), !m_bScanApps);
        }
        else
        {
            addData(strFilePath, size);
        }
    }
}

//-----------------------------------------------------------------------------
/**
** Slot which gets called when a file has been found.
**
** \param strFilename The name of the file.
** \param strFilePath The file path.
** \param size The size of the file.
** \param level The current hierarchy level.
*/
void CScanWindow::onFileFound(const QString& strFilename, const QString& strFilePath, quint64 size, int level)
{
    if (m_pCurrentItem)
    {
        addData(strFilePath, size);

        if (level <= m_Limit)
        {
            QStandardItem* pItem = new QStandardItem();
            pItem->setData(TYPE_FILE, TypeRole);
            pItem->setData(strFilePath, PathRole);
            pItem->setData(strFilename, Qt::DisplayRole);
            pItem->setData(size, SizeRole);
            m_pCurrentItem->appendRow(pItem);
        }
    }
}

//-----------------------------------------------------------------------------
/**
** Adds the size and color to the current item.
**
** \param strFilePath The file path.
** \param size The size of the file.
*/
void CScanWindow::addData(const QString& strFilePath, quint64 size)
{
    if (m_pCurrentItem)
    {
        ColorMap mapSizes;
        quint64 total = m_pCurrentItem->data(SizeRole).toULongLong(NULL);
        quint64 maxSize = m_pCurrentItem->data(MaxSizeRole).toULongLong(NULL);
        QVariant vntSizes = m_pCurrentItem->data(SizeMapRole);
        if (vntSizes.canConvert<ColorMap>())
        {
            mapSizes = vntSizes.value<ColorMap>();
        }

        Colors color = CMainWindow::getColorForPath(strFilePath);
        total += size;
        mapSizes[color] += size;
        maxSize = qMax(maxSize, size);

        vntSizes.setValue(mapSizes);
        m_pCurrentItem->setData(vntSizes, SizeMapRole);
        m_pCurrentItem->setData(total, SizeRole);
        m_pCurrentItem->setData(maxSize, MaxSizeRole);
    }
}

//-----------------------------------------------------------------------------
/**
** Slot which gets called when the selection changes in the tree widget.
** Changes the highlight in the usage grid.
**
** \param current The currently selected item.
** \param previous The previouslu selected item.
*/
void CScanWindow::onItemSelectionChanged(const QModelIndex& current, const QModelIndex& /*previous*/)
{
    if (m_pTreeWidget && m_pDelegate)
    {
        if (current.isValid())
        {
            // Make sure the current is either the selected index or a child of it
            QModelIndex index = current;
            while (index.isValid() && m_pDelegate->selectedIndex().isValid())
            {
                if (index == m_pDelegate->selectedIndex())
                {
                    break;
                }
                index = index.parent();
            }

            if (index.isValid())
            {
                m_pUsageGrid->setSelectionRect(current.data(AreaRole).toRect());
            }
            else
            {
                m_pUsageGrid->setSelectionRect(QRect());
            }

            QString strPath;
            QStandardItem* pItem = m_pModel->itemFromIndex(m_pSortModel->mapToSource(current));

            if (m_bScanApps)
            {
                strPath = pItem->data(Qt::DisplayRole).toString();
            }
            else
            {
                while (pItem)
                {
                    if (!strPath.isEmpty())
                    {
                        strPath.insert(0, "/");
                    }
                    strPath = pItem->data(Qt::DisplayRole).toString() + strPath;
                    pItem = pItem->parent();
                }
            }
            m_pPathLabel->setText(strPath);
        }
        else
        {
            m_pUsageGrid->setSelectionRect(QRect());
            m_pPathLabel->setText(QString());
        }
    }
}

//-----------------------------------------------------------------------------
/**
** Slot which gets called when the an item is expanded. Scans the files for
** the unscanned branches.
**
** \param index The expanded item.
*/
void CScanWindow::onItemExpanded(const QModelIndex& index)
{
    if (index.data(TypeRole).toInt() == TYPE_UNSCANNED_FOLDER)
    {
        QStandardItem* pParent = m_pModel->itemFromIndex(m_pSortModel->mapToSource(index));

        if (pParent)
        {
            // Remove the temp item
            if (pParent->rowCount() == 1 && pParent->child(0, 0)->data(TypeRole).isNull())
            {
                pParent->removeRow(0);
            }

            QDir dir(index.data(PathRole).toString());
            dir.setFilter(QDir::Files | QDir::Hidden | QDir::NoSymLinks);   // We already have the folders

            QFileInfoList list = dir.entryInfoList();
            for (int i = 0; i < list.size(); ++i)
            {
                QFileInfo fileInfo = list.at(i);

                QStandardItem* pItem = new QStandardItem();
                pItem->setData(TYPE_FILE, TypeRole);
                pItem->setData(fileInfo.absoluteFilePath(), PathRole);
                pItem->setData(fileInfo.fileName(), Qt::DisplayRole);
                pItem->setData(fileInfo.size(), SizeRole);

                pParent->appendRow(pItem);
            }

            pParent->setData(TYPE_FOLDER, TypeRole);       // This is now scanned
        }
    }
}

//-----------------------------------------------------------------------------
/**
** Slot which gets called when the an item is double clicked. Changes the
** usage grid to contain only the selected item.
**
** \param index The double clicked item.
*/
void CScanWindow::onItemDoubleClicked(const QModelIndex& index)
{
    if (index.data(TypeRole).toInt() == TYPE_FOLDER)
    {
        if (!index.parent().isValid())
        {
            // Root item -> Keep it expanded
            m_pTreeWidget->collapse(index);     // Collapse so it gets re-expanded on the double click
        }

        m_pDelegate->setSelectedIndex(index);

        if (m_pUsageGrid)
        {
            QStandardItem* pItem = m_pModel->itemFromIndex(m_pSortModel->mapToSource(index));
            bool bRecursive = !m_bScanApps;
            if (m_bScanApps && pItem->parent())    // If an app is selected as shown in the graph we need to do the image recursively
            {
                bRecursive = true;
            }

            m_pUsageGrid->initialize(pItem, bRecursive);
        }
    }
}

//-----------------------------------------------------------------------------
/**
** Slot which gets called when a new maximum value has been set for the progress.
**
** \param maxValue The new maximum progress value.
*/
void CScanWindow::onProgressMaximum(int maxValue)
{
    if (m_pProgress && m_pPathLabel && m_pCancelButton)
    {
        m_pProgress->show();
        m_pCancelButton->show();
        m_pPathLabel->hide();
        m_pProgress->setMaximum(maxValue);
        m_pSearchAction->setEnabled(false);
    }
}

//-----------------------------------------------------------------------------
/**
** Slot which gets called when the progress value changes.
**
** \param strText The progress text.
** \param value The new progress value
*/
void CScanWindow::onProgressValue(QString strText, int value)
{
    if (m_pProgress && m_pPathLabel && m_pCancelButton)
    {
        if (value == -1)    // -1 means end of operation
        {
            m_pProgress->hide();
            m_pCancelButton->hide();
            m_pPathLabel->show();
            m_pSearchAction->setEnabled(true);
        }
        else
        {
            m_pProgress->setValue(value);
            if (m_bScanApps)
            {
                m_pProgress->setFormat(strText + " (%p%)");
            }
            else
            {
                m_pProgress->setFormat(strText);
            }
        }
    }
}

//-----------------------------------------------------------------------------
/**
** Deletes the window when it is closed.
*/
void CScanWindow::closeEvent(QCloseEvent* event)
{
    deleteLater();
}

// EOF
